// app/api/data-room/[projectId]/[fileId]/download/route.ts import { NextRequest, NextResponse } from 'next/server'; import { getServerSession } from 'next-auth/next'; import { authOptions } from '@/app/api/auth/[...nextauth]/route'; import { FileService, type FileAccessContext } from '@/lib/services/fileService'; import { promises as fs } from 'fs'; import path from 'path'; import db from "@/db/db"; import { fileItems } from "@/db/schema/fileSystem"; import { eq } from "drizzle-orm"; export async function GET( request: NextRequest, { params }: { params: { projectId: string; fileId: string } } ) { try { const session = await getServerSession(authOptions); if (!session?.user) { return NextResponse.json({ error: '인증이 필요합니다' }, { status: 401 }); } const context: FileAccessContext = { userId: Number(session.user.id), userDomain: session.user.domain || 'partners', userEmail: session.user.email, ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, userAgent: request.headers.get('user-agent') || undefined, }; const fileService = new FileService(); // 파일 접근 권한 확인 const hasAccess = await fileService.checkFileAccess( params.fileId, context, 'download' ); if (!hasAccess) { return NextResponse.json( { error: '파일 다운로드 권한이 없습니다' }, { status: 403 } ); } // FileService를 통해 파일 정보 가져오기 (다운로드 카운트 증가 및 로그 기록) const file = await fileService.downloadFile(params.fileId, context); if (!file) { return NextResponse.json( { error: '파일을 찾을 수 없습니다' }, { status: 404 } ); } // 파일 경로 확인 if (!file.filePath) { return NextResponse.json( { error: '파일 경로가 없습니다' }, { status: 404 } ); } // 실제 파일 경로 구성 const nasPath = process.env.NAS_PATH || "/evcp_nas"; const isProduction = process.env.NODE_ENV === "production"; let absolutePath: string; if (isProduction) { // 프로덕션: NAS 경로 사용 const relativePath = file.filePath.replace('/api/files/', ''); absolutePath = path.join(nasPath, relativePath); } else { // 개발: public 폴더 사용 absolutePath = path.join(process.cwd(), 'public', file.filePath); } // 파일 존재 여부 확인 try { await fs.access(absolutePath); } catch (error) { console.error('파일을 찾을 수 없습니다:', absolutePath); return NextResponse.json( { error: '파일을 찾을 수 없습니다' }, { status: 404 } ); } // 파일 읽기 const fileBuffer = await fs.readFile(absolutePath); // MIME 타입 결정 const mimeType = getMimeType(file.name, file.mimeType); // 파일명 인코딩 (한글 등 특수문자 처리) const encodedFileName = encodeURIComponent(file.name); // Response Headers 설정 const headers = new Headers(); headers.set('Content-Type', mimeType); headers.set('Content-Length', fileBuffer.length.toString()); headers.set('Content-Disposition', `attachment; filename*=UTF-8''${encodedFileName}`); headers.set('Cache-Control', 'no-cache, no-store, must-revalidate'); headers.set('Pragma', 'no-cache'); headers.set('Expires', '0'); // 보안 헤더 추가 headers.set('X-Content-Type-Options', 'nosniff'); headers.set('X-Frame-Options', 'DENY'); headers.set('X-XSS-Protection', '1; mode=block'); // 파일 스트림 반환 return new NextResponse(fileBuffer, { status: 200, headers, }); } catch (error) { console.error('파일 다운로드 오류:', error); if (error instanceof Error) { if (error.message.includes('권한')) { return NextResponse.json( { error: error.message }, { status: 403 } ); } } return NextResponse.json( { error: '파일 다운로드에 실패했습니다' }, { status: 500 } ); } } // HEAD 요청 처리 (파일 정보만 확인) export async function HEAD( request: NextRequest, { params }: { params: { projectId: string; fileId: string } } ) { try { const session = await getServerSession(authOptions); if (!session?.user) { return new NextResponse(null, { status: 401 }); } const context: FileAccessContext = { userId: Number(session.user.id), userDomain: session.user.domain || 'partners', userEmail: session.user.email, ipAddress: request.ip || request.headers.get('x-forwarded-for') || undefined, userAgent: request.headers.get('user-agent') || undefined, }; const fileService = new FileService(); // 파일 접근 권한 확인 const hasAccess = await fileService.checkFileAccess( params.fileId, context, 'view' // HEAD 요청은 view 권한만 확인 ); if (!hasAccess) { return new NextResponse(null, { status: 403 }); } // 파일 정보 조회 const file = await db.query.fileItems.findFirst({ where: eq(fileItems.id, params.fileId), }); if (!file || !file.filePath) { return new NextResponse(null, { status: 404 }); } const headers = new Headers(); headers.set('Content-Type', getMimeType(file.name, file.mimeType)); headers.set('Content-Length', file.size?.toString() || '0'); headers.set('Last-Modified', new Date(file.updatedAt).toUTCString()); return new NextResponse(null, { status: 200, headers, }); } catch (error) { console.error('HEAD 요청 오류:', error); return new NextResponse(null, { status: 500 }); } } // MIME 타입 결정 헬퍼 함수 function getMimeType(fileName: string, storedMimeType?: string | null): string { // DB에 저장된 MIME 타입이 있으면 우선 사용 if (storedMimeType) { return storedMimeType; } // 확장자 기반 MIME 타입 매핑 const ext = path.extname(fileName).toLowerCase().substring(1); const mimeTypes: Record = { // Documents 'pdf': 'application/pdf', 'doc': 'application/msword', 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'xls': 'application/vnd.ms-excel', 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'ppt': 'application/vnd.ms-powerpoint', 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'txt': 'text/plain', 'csv': 'text/csv', // Images 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'png': 'image/png', 'gif': 'image/gif', 'bmp': 'image/bmp', 'webp': 'image/webp', 'svg': 'image/svg+xml', // Archives 'zip': 'application/zip', 'rar': 'application/x-rar-compressed', '7z': 'application/x-7z-compressed', // CAD 'dwg': 'application/x-dwg', 'dxf': 'application/x-dxf', // Video 'mp4': 'video/mp4', 'avi': 'video/x-msvideo', 'mov': 'video/quicktime', 'wmv': 'video/x-ms-wmv', // Audio 'mp3': 'audio/mpeg', 'wav': 'audio/wav', 'ogg': 'audio/ogg', }; return mimeTypes[ext] || 'application/octet-stream'; }